/*
 * Copyright (c) 2009. Silenus Consultoria, S.L.
 */
package com.tonbeller.wcf.utils;

import net.sf.saxon.Configuration;
import net.sf.saxon.TransformerFactoryImpl;

import org.apache.log4j.Logger;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.io.Writer;

import java.net.MalformedURLException;
import java.net.URL;

import java.util.Hashtable;
import java.util.Properties;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;


/**
 * DOCUMENT ME!
 *
 * @author av XML Utilities
 */
public class XmlUtils implements ErrorListener {
	private static final String WEBKEY = XmlUtils.class.getName();
	private static Logger logger = Logger.getLogger(XmlUtils.class);

	/**
	 * Xalan Templates are not thread safe (as the spec requires), so we have one instance of the templatesCache
	 * for every http session.
	 */
	private Hashtable templatesCache = new Hashtable();

	private XmlUtils() {
	}

	/**
	 * creates a transformer for a xsl stylesheet
	 *
	 * @param ctx the context.
	 * @param xslUri the uri of the xslt stylesheet, e.g. "/WEB-INF/mystyle.xsl"
	 * @param xslCache cache the transformer
	 *
	 * @return the transformer.
	 *
	 * @throws SoftException if something goes wrong.
	 */
	public Transformer getTransformer(ServletContext ctx, String xslUri, boolean xslCache) {
		synchronized(templatesCache) {
			try {
				Templates templates = null;

				if(xslCache) {
					templates = (Templates)templatesCache.get(xslUri);
				}

				if(templates == null) {
					TransformerFactory tf = TransformerFactory.newInstance();

					// FIX: added to support saxon 9
					if(tf instanceof TransformerFactoryImpl) {
						Configuration config = new Configuration();

						config.setRecoveryPolicy(Configuration.RECOVER_SILENTLY);
						config.setValidationWarnings(true);
						config.setErrorListener(this);

						TransformerFactoryImpl saxonTf = (TransformerFactoryImpl)tf;

						saxonTf.setConfiguration(config);
					}

					URL url = ctx.getResource(xslUri);

					if(url == null) {
						throw new IllegalArgumentException("stylesheet \"" + xslUri + "\" not found");
					}

					StreamSource ss = new StreamSource(url.toExternalForm());

					// BEA 8.1 needs SystemID to resolve includes
					if("file".equals(url.getProtocol())) {
						File f = new File(url.getFile());

						ss.setSystemId(f);
					}

					templates = tf.newTemplates(ss);

					if(xslCache) {
						templatesCache.put(xslUri, templates);
					}
				}

				return templates.newTransformer();
			} catch(TransformerConfigurationException e) {
				throw new SoftException("Error building transformer for: " + xslUri, e);
			} catch(MalformedURLException e) {
				throw new SoftException("Error building transformer for: " + xslUri, e);
			}
		}
	}

	/**
	 * Creates a transformer.
	 *
	 * @param session the session.
	 * @param xslUri the xsl uri.
	 * @param xslCache the xsl cache.
	 *
	 * @return the transformer.
	 */
	public static Transformer getTransformer(HttpSession session, String xslUri, boolean xslCache) {
		return instance(session).getTransformer(session.getServletContext(), xslUri, xslCache);
	}

	/**
	 * Retrieves the parser.
	 *
	 * @return the parser.
	 *
	 * @throws SoftException if something goes wrong.
	 */
	public static DocumentBuilder getParser() {
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

			dbf.setValidating(false);
			dbf.setExpandEntityReferences(true);

			return dbf.newDocumentBuilder();
		} catch(FactoryConfigurationError e) {
			throw new SoftException(e);
		} catch(ParserConfigurationException e) {
			throw new SoftException(e);
		}
	}

	/**
	 * Creates a document.
	 *
	 * @return the document.
	 *
	 * @throws SoftException if something goes wrong.
	 */
	public static Document createDocument() {
		try {
			return getParser().newDocument();
		} catch(FactoryConfigurationError e) {
			throw new SoftException(e);
		}
	}

	/**
	 * Prints a node.
	 *
	 * @param node the node.
	 * @param out the writer.
	 * @param p the output properties.
	 *
	 * @throws SoftException if something goes wrong.
	 */
	public static void print(Node node, Writer out, Properties p) {
		try {
			TransformerFactory tf = TransformerFactory.newInstance();
			Source src = new DOMSource(node);
			Result dest = new StreamResult(out);
			Transformer t = tf.newTransformer();

			if(p != null) {
				t.setOutputProperties(p);
			}

			t.transform(src, dest);
		} catch(TransformerConfigurationException e) {
			throw new SoftException(e);
		} catch(TransformerFactoryConfigurationError e) {
			throw new SoftException(e);
		} catch(TransformerException e) {
			throw new SoftException(e);
		}
	}

	/**
	 * Prints a node.
	 *
	 * @param node the node.
	 * @param out the writer.
	 */
	public static void print(Node node, Writer out) {
		print(node, out, null);
	}

	/**
	 * Creates an instance per session.
	 *
	 * @param session the session.
	 *
	 * @return the instance.
	 */
	public static synchronized XmlUtils instance(HttpSession session) {
		XmlUtils service = (XmlUtils)session.getAttribute(WEBKEY);

		if(service == null) {
			service = new XmlUtils();
			session.setAttribute(WEBKEY, service);
		}

		return service;
	}

	/**
	 * returns the Document. node itself may be a Document node in which case node.getOwnerDocument() will return
	 * null
	 *
	 * @param node the node.
	 *
	 * @return the document.
	 */
	public static Document getDocument(Node node) {
		if(node.getNodeType() == Node.DOCUMENT_NODE) {
			return (Document)node;
		}

		return node.getOwnerDocument();
	}

	/**
	 * Parses a document from an URL.
	 *
	 * @param url the URL.
	 *
	 * @return the parsed document.
	 *
	 * @throws SoftException if something goes wrong.
	 */
	public static Document parse(URL url) {
		try {
			InputSource src = new InputSource(url.toExternalForm());

			return getParser().parse(src);
		} catch(IOException e) {
			throw new SoftException(e);
		} catch(SAXException e) {
			throw new SoftException(e);
		}
	}

	/**
	 * escapes xml characters &lt;, &gt; &amp;, &quot;
	 *
	 * @param s the text.
	 *
	 * @return the escaped text.
	 */
	public static String escapeXml(String s) {
		if(s == null) {
			return null;
		}

		char[] arr = s.toCharArray();
		StringBuffer sb = new StringBuffer();

		for(int i = 0; i < arr.length; i++) {
			switch(arr[i]) {
				case '>':
					sb.append("&gt;");

					break;

				case '<':
					sb.append("&lt;");

					break;

				case '&':
					sb.append("&amp;");

					break;

				case '"':
					sb.append("&quot;");

					break;

				default:
					sb.append(arr[i]);
			}
		}

		return sb.toString();
	}

	// FIX: added to support saxon 9
	/**
	 * Receive notification of a warning. {@link javax.xml.transform.Transformer} can use this method to report
	 * conditions that are not errors or fatal errors.  The default behaviour is to take no action. After invoking this
	 * method, the Transformer must continue with the transformation. It should still be possible for the application to
	 * process the document through to the end.
	 *
	 * @param exception The warning information encapsulated in a transformer exception.
	 *
	 * @throws TransformerException if the application chooses to discontinue the transformation.
	 *
	 * @see javax.xml.transform.TransformerException
	 */
	public void warning(TransformerException exception) throws TransformerException {
		logger.warn(exception);
	}

	/**
	 * Receive notification of a recoverable error. The transformer must continue to try and provide normal
	 * transformation after invoking this method.  It should still be possible for the application to process the
	 * document through to the end if no other errors are encountered.
	 *
	 * @param exception The error information encapsulated in a transformer exception.
	 *
	 * @throws TransformerException if the application chooses to discontinue the transformation.
	 *
	 * @see javax.xml.transform.TransformerException
	 */
	public void error(TransformerException exception) throws TransformerException {
		logger.error(exception);
	}

	/**
	 * Receive notification of a non-recoverable error. The processor may choose to continue, but will not normally
	 * proceed to a successful completion. The method should throw an exception if it is unable to process the error, or
	 * if it wishes execution to terminate immediately. The processor will not necessarily honor this request.
	 *
	 * @param exception The error information encapsulated in a <code>TransformerException</code>.
	 *
	 * @throws TransformerException if the application chooses to discontinue the transformation.
	 *
	 * @see javax.xml.transform.TransformerException
	 */
	public void fatalError(TransformerException exception) throws TransformerException {
		logger.error(exception);
	}
}
